﻿/*
	Class: DeepLinkManager
	
		This class is used to interface with the SWFAddress deep-linking module.
		If SWFAddress is not compiled into the SWF then the functions below do
		nothing.
	
	Note:
	
		In the past, we had problems with SWFAddress and had to address them by
		altering the JS file. For example, if you see the site reloading when
		deep linking to a section with somewhat odd characters in it (like a 
		colon) then consider editing the _getHash function and removing the 
		regular expression filter.
*/
package com.frontalcode
{
	import flash.events.*;
	import flash.utils.*;
	import flash.external.*;
	
	/**                  
     * The DeepLinkManager class integrates with SWFAddress to support deep
	 * linking in Frontal documents.
     */
	public class DeepLinkManager extends OptionableObject
	{
		static private var _instance : DeepLinkManager;
		static private var _SWFAddress : Class;
		
		// For each document, we create a parameter like d# set to its 
		// identifier. Then for each deep-linked DocumentElement in that 
		// document, we create a parameter like d#m# set to its identifier. And 
		// finally, for each DocumentElement's selection, we create a parameter 
		// like d#m#e set to that selection's identifier. We use paramIndices to 
		// help create and manage these parameters.
		//
		/**            
		 * @private
		 */
		static internal var paramIndices : Object = { nextIndex: 1, entries: new Dictionary ( true ) };
		
		// Only one Container should have the deep-link-in-path style set to 
		// true. This variable tracks this manager.
		//
		// XXX We need a way to control the situation that multiples 
		// DocumentElements have the deep-link-in-path style set to true. Right 
		// now we issue a warning in the Debugger.
		//
		static private var _pathManager : DocumentElement;
		
		// Avoid handling callbacks created by our calling 
		// onContainerManagerChange and vice versa.
		//
		static private var inUse : Boolean = false;
		
		
		
		/*
			Variables: initialPath, initialQuery, initialized
			
				These variables store the initial deep link.
		*/
		static public var initialPath : String;
		static public var initialQuery : Object;
		static public var initialized : Boolean = false;
		
		/**            
		 * @private
		 */
		public function DeepLinkManager ( options : Object = null )
		{
			super ( options );
			_instance = this;
			setOptionsFromLoaderParameters ( );
			
			// Try to initialize things.
			//
			SWFAddress;
		}
		
		/**
		 * The single instance of the DeepLinkManager class.
		 *
		 * @return The deep link manager.
		 */
		static public function getInstance ( ) : DeepLinkManager
		{
			if ( _instance == null ) throw new FrontalError ( "DeepLinkManager not initialized" );
			return _instance;
		}
		
		// Convenience method.
		//
		/**
		 * The single instance of the DeepLinkManager class.
		 *
		 * @return The deep link manager.
		 */
		static public function gI ( ) : DeepLinkManager { return getInstance ( ); }
		
		/**            
		 * @private
		 */
		static public function get SWFAddress ( ) : Class
		{
			if ( _SWFAddress == null )
			{
				try
				{
					_SWFAddress = getDefinitionByName ( "SWFAddress" ) as Class;
					_SWFAddress [ "addEventListener" ] ( "init", swfAddressChangeHandler );
					_SWFAddress [ "addEventListener" ] ( "change", swfAddressChangeHandler );
				}
				catch ( e : Error )
				{
				}
			}
			
			return _SWFAddress;
		}
		
		/**            
		 * @private
		 */
		static internal function set pathManager ( value : DocumentElement ) : void
		{
			if ( _pathManager != value )
			{
				if ( _pathManager != null ) 
				{
					Debugger.logMessage ( Debugger.ERROR, "DeepLinkManager", "at most one element should have the style deep-link-in-path set to true" );
					getInstance ( ).removeListener ( _pathManager, FrontalEvent.REMOVE_ELEMENT, pathManagerDestroyedHandler );
				}
				_pathManager = value;
				if ( value != null ) getInstance ( ).addListener ( _pathManager, FrontalEvent.REMOVE_ELEMENT, pathManagerDestroyedHandler );
			}
		}
		
		/**            
		 * @private
		 */
		static internal function get pathManager ( ) : DocumentElement
		{
			return _pathManager;
		}
		
		/**            
		 * @private
		 */
		static internal function pathManagerDestroyedHandler ( event : FrontalEvent ) : void
		{
			if ( event.target === pathManager ) pathManager = null;
		}
		
		/**            
		 * @private
		 */
		static internal function getDocumentIndex ( document : Document ) : uint
		{
			if ( paramIndices.entries [ document ] == null ) paramIndices.entries [ document ] = { index: paramIndices.nextIndex++, nextIndex: 1, entries: new Dictionary ( true ), reservedIndexes: { }, idToReservedIndexMap: { } };
			return paramIndices.entries [ document ].index;
		}
		
		/**            
		 * @private
		 */
		static internal function getDocumentElementIndex ( mgr : DocumentElement ) : uint
		{
			getDocumentIndex ( mgr.document );
			var docEntry : Object = paramIndices.entries [ mgr.document ];
			if ( docEntry.entries [ mgr ] == null ) 
			{
				var index : Number;
				if ( docEntry.idToReservedIndexMap.hasOwnProperty ( mgr.node.@id ) )
				{
					index = docEntry.idToReservedIndexMap [ mgr.node.@id ];
				}
				else
				{
					index = docEntry.nextIndex++;
					while ( docEntry.reservedIndexes.hasOwnProperty ( index ) ) index = docEntry.nextIndex++;
				}
				setDocumentElementIndex ( mgr.document, mgr, mgr.node.@id, index );
			}
			return docEntry.entries [ mgr ];
		}
		
		/**            
		 * @private
		 */
		static internal function setDocumentElementIndex ( document : Document, mgr : DocumentElement, mgrId : String, index : uint ) : void
		{
			getDocumentIndex ( document );
			var docEntry : Object = paramIndices.entries [ document ];
			if ( ! docEntry.reservedIndexes.hasOwnProperty ( index ) )
			{
				docEntry.reservedIndexes [ index ] = true;
				docEntry.idToReservedIndexMap [ mgrId ] = index;
			}
			if ( mgr != null ) docEntry.entries [ mgr ] = index;
		}
		
		/*
			Function: swfAddressChangeHandler
			
				SWFAddress is reporting initialization or a change in the 
				address.
		*/
		/**            
		 * @private
		 */
		static internal function swfAddressChangeHandler ( event : Event ) : void 
		{
			if ( event.type == "init" )
			{
				initialPath = event [ "path" ];
				initialQuery = { };
				for each ( var name : String in event [ "parameterNames" ] ) initialQuery [ name ] = event [ "parameters" ] [ name ];
				initialized = true;
				_SWFAddress [ "removeEventListener" ] ( "init", swfAddressChangeHandler );
				getInstance ( ).dispatchEvent ( new FrontalEvent ( FrontalEvent.INITIALIZED ) );
				return;
			}
			
			jumpToDeepLink ( ); 
		}
		
		/*
			Function: saveInitialState
			
				SWFAddress does not provide us with a way to tell if it has been
				initialized or not. This is particularly problematic as 
				SWFAddress waits 10ms to initialize itself. So, are we here 
				before or after SWFAddress has initialized? To guess, we make a
				JS call to get the path and query string ourself. If these seem
				to be what SWFAddress is returning then we assume that we are
				here after SWFAddress has been initialized.
		*/
		/**            
		 * @private
		 */
		static private function saveInitialState ( ) : void
		{
			if ( initialized || SWFAddress == null ) return;
			
			if ( ExternalInterface.available )
			{
                var value : String = ExternalInterface.call ( 'SWFAddress.getValue' ) as String;
				if ( SWFAddress [ "getStrict" ] ( ) && value == '' ) value = '/';
				value = decodeURI ( value );
				
				// If the value from the AS SWFAddress doesn't match that from 
				// the JS SWFAddress then we assume things aren't initialized.
				//
				if ( value != SWFAddress [ "getValue" ] ( ) ) return;
			}
			
			initialPath = SWFAddress [ "getPath" ] ( );
			initialQuery = { };
			for each ( var name : String in SWFAddress [ "getParameterNames" ] ( ) ) initialQuery [ name ] = SWFAddress [ "getParameter" ] ( name );
			initialized = true;
			_SWFAddress [ "removeEventListener" ] ( "init", swfAddressChangeHandler );
			getInstance ( ).dispatchEvent ( new FrontalEvent ( FrontalEvent.INITIALIZED ) );
		}
		
		/*
			Function: jumpToDeepLink
			
				Parse the deep link and jump to the appropriate sections.
		*/
		/**            
		 * @private
		 */
		static internal function jumpToDeepLink ( ) : void { internalJumpToOrGetDeepLink ( ); }
		
		/*
			Function: getDeepLink
			
				Parse the deep link and return the value that the managing
				DocumentElement has assigned to it or undefined if there's no 
				relevant deep link.
				
			Parameters:
			
				mgr		- Return the deep-linked object for this DocumentElement 
						  or undefined if there is none.
						  
			Returns:
			
				A value or undefined. See the description of the mgr parameter.
		*/
		/**            
		 * @private
		 */
		static internal function getDeepLink ( mgr : DocumentElement, useInitialState : Boolean = false ) : * { return internalJumpToOrGetDeepLink ( mgr, useInitialState ); }
		
		/*
			Function: internalJumpToOrGetDeepLink
			
				Parse the deep link and jump to the appropriate sections. If mgr
				is set, then just return the Container that we would jump to for
				this manager or null if there's no relevant deep link.
				
			Parameters:
			
				mgr				- Return the deep-linked object for this 
								  DocumentElement or undefined if there is none.
				useInitialState	- If true then use the initial deep-link state
								  otherwise use the current state.
						  
			Returns:
			
				A value or undefined. See the description of the mgr parameter.
		*/
		/**            
		 * @private
		 */
		static internal function internalJumpToOrGetDeepLink ( mgr : DocumentElement = null, useInitialState : Boolean = false ) : *
		{
			if ( ! initialized ) saveInitialState ( );
			if ( SWFAddress == null || inUse || ! initialized ) return null;
			
			if ( mgr != null && mgr.getStyle ( "deep-link" ) == "path" ) pathManager = mgr;
			
			if ( mgr == null || mgr === pathManager )
			{
				var path : String = useInitialState ? initialPath : SWFAddress [ "getPath" ] ( );
				if ( path != null && path.length > 0 && pathManager != null )
				{
					if ( mgr != null ) return mgr.getDeepLinkValue ( path );
					pathManager.processDeepLink ( pathManager.getDeepLinkValue ( path ) );
				}
			}
			
			var elementIndicator : RegExp = /^d(\d+)m(\d+)e$/; 
			var names : Array = [ ];
			if ( useInitialState )
			{
				for ( var name : String in initialQuery ) names.push ( name );
			}
			else
			{
				names = SWFAddress [ "getParameterNames" ] ( );
			}
			
			var result : *;
			
			for each ( name in names )
			{
				var match : Object = elementIndicator.exec ( name );
				if ( match != null )
				{
					var docParam  : String = "d" + match [ 1 ];
					var mgrIndex  : uint   = uint ( match [ 2 ] );
					var mgrParam  : String = docParam + "m" + mgrIndex;
					var elemParam : String = mgrParam + "e";
					
					var docValue  : String = useInitialState ? initialQuery [ docParam  ] : SWFAddress [ "getParameter" ] ( docParam  );
					var mgrValue  : String = useInitialState ? initialQuery [ mgrParam  ] : SWFAddress [ "getParameter" ] ( mgrParam  );
					var elemValue : String = useInitialState ? initialQuery [ elemParam ] : SWFAddress [ "getParameter" ] ( elemParam );
					
					var document : Document = DocumentManager.getInstance ( ).getDocument ( docValue );
					if ( document != null )
					{
						// Store this manager in our paramIndices.
						//
						setDocumentElementIndex ( document, null, mgrValue, mgrIndex );
						
						if ( mgr == null )
						{
							// Don't set mgr because the fact it's null affects
							// our behavior in the next iteration of the loop.
							//
							var linkMgr : DocumentElement = document.getElementById ( mgrValue, false );
							if ( linkMgr != null ) linkMgr.processDeepLink ( linkMgr.getDeepLinkValue ( unescape ( elemValue ) ) );
						}
						else if ( mgr.node.@id == mgrValue )
						{
							result = mgr.getDeepLinkValue ( unescape ( elemValue ) );
						}
					}
				}
			}
			
			return result;
		}

		/*
			Function: updateDeepLink
			
				When a DocumentElement that is a deep link handler has a change
				in its state, it reports that change through this method. This
				method then builds the new deep link value and passes it to
				SWFAddress to update the browser.
				
				We gather all the calls during this frame and then combine them
				to avoid extra entries in our history. This handles the use case 
				that we are changing multiple manager selections simultaneously.
				
			Parameters:
			
				index	- The index associated with the DocumentElement's new 
						  state. If the DocumentElement's deep-link style is set
						  to "path" then this index should be suitably formatted
						  for that purpose otherwise it will be the value of a
						  d#m#e query parameter.
				title	- The title of the DocumentElement's new state. This is
						  only used if the DocumentElement's deep-link style is 
						  set to "path" and in that case if the title is not 
						  null, it will be used to update the browser's title. 
		*/
		
		static private var updates : Array = [ ];
		static private var updateDeepLinkTaskId : uint = 0;
		
		/**            
		 * @private
		 */
		static public function updateDeepLink ( mgr : DocumentElement, index : String, title : String = null ) : void
		{
			addUpdate ( { type: "deepLink", mgr: mgr, index: index, title: title } );
		}
			
		/**            
		 * @private
		 */
		static public function setParameter ( parameter : String, value : String ) : void
		{
			addUpdate ( { type: "parameter", parameter: parameter, value: value } );
		}
		
		/**            
		 * @private
		 */
		static public function getParameter ( parameter : String ) : String
		{
			if ( ! initialized ) saveInitialState ( );
			if ( SWFAddress == null || inUse || ! initialized ) null;
			
			for each ( var entry : Object in updates )
			{
				if ( entry.type == "parameter" && entry.parameter == parameter ) return entry.remove ? null : entry.value;
			}
			
			return SWFAddress [ "getParameter" ] ( parameter );
		}
		
		/**            
		 * @private
		 */
		static public function removeParameter ( parameter : String ) : void
		{
			addUpdate ( { type: "parameter", parameter: parameter, remove: true } );
		}
		
		static private function addUpdate ( update : Object ) : void
		{
			if ( ! initialized ) saveInitialState ( );
			if ( SWFAddress == null || inUse || ! initialized ) return;
			
			updates.push ( update );

			if ( updateDeepLinkTaskId == 0 ) updateDeepLinkTaskId = Scheduler.setTimeoutInFrames ( processDeepLinkUpdates, 1 );
		}
		
		/**            
		 * @private
		 */
		static internal function processDeepLinkUpdates ( ) : void
		{
			updateDeepLinkTaskId = Scheduler.getInstance ( ).removeTask ( updateDeepLinkTaskId );
			
			var path : String;
			var title : String;
			
			var query : Object = { };
			var queryChanged : Boolean = false;
			for each ( var name : String in SWFAddress [ "getParameterNames" ] ( ) ) query [ name ] = SWFAddress [ "getParameter" ] ( name );
			
			for each ( var entry : Object in updates )
			{
				if ( entry.type == "deepLink" )
				{
					if ( entry.mgr.getStyle ( "deep-link" ) == "path" )
					{
						pathManager = entry.mgr;
						path = entry.index;
						title = entry.title;
					}
					else
					{
						// See the discussion about paramIndices above to understand the
						// three parameters we create here.
						//
						var docParam : String = "d" + getDocumentIndex ( entry.mgr.document );
						var mgrParam : String = docParam + "m" + getDocumentElementIndex ( entry.mgr );
						query [ docParam ] = entry.mgr.document.title;
						query [ mgrParam ] = entry.mgr.node.@id;
						query [ mgrParam + "e" ] = entry.index;
					}
				}
				else // "parameter"
				{
					if ( entry.remove )
					{
						if ( query.hasOwnProperty ( entry.parameter ) )
						{
							queryChanged = true;
							delete query [ entry.parameter ];
						}
					}
					else
					{
						query [ entry.parameter ] = entry.value;
					}
				}
			}
			
			updates = [ ];

			var queryString : String = "";
			for ( name in query )
			{
				if ( queryString.length > 0 ) queryString += "&";
				queryString += name + "=" + escape ( query [ name ] );
				if ( query [ name ] != unescape ( SWFAddress [ "getParameter" ] ( name ) ) ) queryChanged = true;
			}
			
			if ( title != null ) 
			{
				var decoratedTitle : * = pathManager.runInteractions ( "setDeepLinkTitle", { title: title } );
				title = decoratedTitle === undefined ? title : decoratedTitle;
				if ( title != null ) SWFAddress [ "setTitle" ] ( title );
			}
			
			if ( queryChanged || path != SWFAddress [ "getPath" ] ( ) )
			{
				if ( path == null ) path = SWFAddress [ "getPath" ] ( );
				if ( ! queryChanged ) queryString = SWFAddress [ "getQueryString" ] ( );
				inUse = true;
				SWFAddress [ "setValue" ] ( path + ( queryString != null && queryString.length > 0 ? "?" + queryString : "" ) );
				inUse = false;
			}
		}
	}
}


